41. Exercise: Using a CursorLoader

CursorLoader

Right now, Sunshine uses an AsyncTaskLoader to load weather data on a background thread, but this loads weather data directly from a JSON response, and not from the data source we just built: the WeatherProvider! So, in the next couple steps, we’ll improve the Sunshine code and take advantage of a different loader class to efficiently to load data from the WeatherProvider. The best way to asynchronously load data from any ContentProvider is with a CursorLoader.

A CursorLoader is a subclass of AsyncTaskLoader that queries a ContentProvider, via a ContentResolver and specific URI, and returns a Cursor of desired data. This loader runs its query on a background thread so that it doesn’t block the UI. When a CursorLoader is active, it is tied to a URI, and you can choose to have it monitor this URI for any changes in data; this means that the CursorLoader can deliver new results whenever the contents of our weather database change, and we can automatically update the UI to reflect any weather change!

In this exercise, you'll replace the AsyncTaskLoader with a CursorLoader

Exercise Code

Exercise: S09.04-Exercise-UsingCursorLoader

Modify the ForecastAdapter

Our first step will be to modify the ForecastAdapter so that it creates views based on the data in a Cursor (and not an array of Strings). So, in the ForecastAdapter class, we’ll replace the global variable private String[] mWeatherData with a Cursor that holds that data: private Cursor mCursor.

Then, we’ll change the onBindViewHolder method so that it takes all of the data from a cursor and uses that to populate the views. To read the correct weather data from a Cursor, you have to read that data from the correct position with a call to: mCursor.moveToPosition(position). Then we’ll extract all the relevant values from the Cursor, and finally set the text to display the correct weather summary. To read in data from a Cursor, you can use methods like getInt or getString on that Cursor, ex. double highInCelsius = mCursor.getDouble(MainActivity.INDEX_WEATHER_MAX_TEMP). Then create a string to summarize the weather String weatherSummary = dateString + " - " + description + " - " + highAndLowTemperature and display it!

You'll also want to update the getItemCount method to reflect the number of items in the Cursor, which you can get with a call to: mCursor.getCount().

And replace the setWeatherData method with a new method called swapCursor, which should take in a new Cursor and update the value of the old Cursor. This is how changes in data will be managed.

Here is the swapCursor method:

void swapCursor(Cursor newCursor) {
        mCursor = newCursor;
        // After the new Cursor is set, call notifyDataSetChanged
        notifyDataSetChanged();
}

ForecastAdapter Changes

Change the ForecastAdapter to read and display data from a Cursor.

SOLUTION:
  • Create a global variable to keep track of the Cursor and replace the array of Strings
  • Change the onBindViewHolder method so that it retreives weather data from the Cursor
  • Create and display a weather summary that describes the retrieved weather data
  • Change getItemCount so that it returns the number of items in the Cursor
  • Create a new method, called swapCursor, that swaps an old Cursor with a new Cursor of data

Create a CursorLoader in MainActivity

The next step will be to update the code in the MainActivity and replace the AsyncTaskLoader with a CursorLoader.

First, you’ll want to change the loader callbacks so that the loader manager knows that it is returning a Cursor: LoaderManager.LoaderCallbacks<Cursor>. You’ll also change the methods for 1) onCreateLoader, 2) onLoadFinished, and 3) onLoaderReset to reflect this change in data structure.

To change the initialization of the loader, we’ll create and return a CursorLoader in the onCreateLoader method. A CursorLoader is created similar to how you create a query - by passing in a URI that points to our data source, with a sortOrder and selection; you’ll also pass in a Context (this) as the first argument. I’ll leave the construction of the projection and deletion to you, but your final loader creation code should look like this:

 return new CursorLoader(this,
                       forecastQueryUri,
                       MAIN_FORECAST_PROJECTION,
                       selection,
                       null,
                       sortOrder);

Next, you'll change the onLoadFinished method and swap the Cursor so that our UI reflects the latest data. You can do this with a call to swapCursor: mForecastAdapter.swapCursor(data) where data is the name of a passed in Cursor.

Finally, in our onLoaderReset method, we should clear out the adapter that is displaying the data with another call to swapCursor that clears the data: mForecastAdapter.swapCursor(null).

CursorLoader

Replace the AsyncTaskLoader with a CursorLoader

SOLUTION:
  • Update the loader callbacks interface and methods to manage a Cursor instead of an array of Strings
  • Write the code for the loader callback method onCreateLoader; make sure it's using a URI for all of the weather data
  • Write the code for the loader callback method onLoadFinished, and update the weather data with a call to swapCursor
  • Update the code for the loader callback method onLoaderReset, and clear the displayed data with another call to swapCursor